package com.jwong.auth.server.sms;

import com.jwong.common.entity.SmsCode;
import com.jwong.common.exception.BaseErrorCode;
import com.jwong.common.exception.ServiceException;
import com.jwong.common.util.RedisKeyGenerator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

@Slf4j
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    private RedisTemplate redisTemplate;

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    protected UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public RedisTemplate getRedisTemplate() {
        return redisTemplate;
    }

    public void setRedisTemplate(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }


    public Authentication authenticate(Authentication authentication) throws AuthenticationException,
            ServiceException {
        Assert.isInstanceOf(SmsCodeAuthenticationToken.class, authentication,
                () -> "Only SmsCodeAuthenticationProvider is supported");
        String mobile = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
        UserDetails user;
        try {
            user = retrieveUser(mobile, (SmsCodeAuthenticationToken) authentication);
        }
        catch (UsernameNotFoundException notFound) {
            log.debug("User '" + mobile + "' not found");
            throw notFound;
        }
        Assert.notNull(user,
                "retrieveUser returned null - a violation of the interface contract");
        Object principalToReturn = user;
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

    protected Authentication createSuccessAuthentication(Object principal,
                                                         Authentication authentication, UserDetails user) {
        SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(
                principal, authentication.getCredentials(), user.getAuthorities());
        result.setDetails(authentication.getDetails());
        return result;
    }

    protected final UserDetails retrieveUser(String username, SmsCodeAuthenticationToken authentication)
            throws AuthenticationException {
        additionalAuthenticationChecks(authentication);
        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

    protected void additionalAuthenticationChecks(SmsCodeAuthenticationToken authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            log.debug("Authentication failed: no smsCode provided");
            throw new BadCredentialsException(BaseErrorCode.SMS_CODE_CAN_NOT_BE_NULL.getErrDispose());
        }

        String smsCode = authentication.getCredentials().toString();
        String mobile = authentication.getName();

        if (!StringUtils.hasText(smsCode)) {
            log.debug("Authentication failed: no smsCode provided");
            throw new BadCredentialsException(BaseErrorCode.SMS_CODE_CAN_NOT_BE_NULL.getErrDispose());
        }
        if (!StringUtils.hasText(mobile)) {
            throw new InternalAuthenticationServiceException(
                    BaseErrorCode.MOBILE_NUMBER_CAN_NOT_BE_NULL.getErrDispose());
        }
        String smsCodeKey = RedisKeyGenerator.smsCodeKey(mobile);
        BoundValueOperations<String, SmsCode> smsCodeOperations = this.getRedisTemplate().boundValueOps(smsCodeKey);
        SmsCode smsCodeInCache = smsCodeOperations.get();
        if (smsCodeInCache == null) {
            throw new BadCredentialsException(BaseErrorCode.SMS_CODE_IS_NOT_EXISTS.getErrDispose());
        }
        if (smsCodeInCache.isExpired()) {
            log.error("Authentication failed: smsCode[{}] does not match stored value.", smsCode);
            throw new BadCredentialsException(BaseErrorCode.SMS_CODE_INPUT_IS_EXPIRE.getErrDispose());
        }
        if (!smsCode.equalsIgnoreCase(smsCodeInCache.getCode())) {
            log.error("Authentication failed: smsCode[{}] does not match stored value.", smsCode);
            throw new BadCredentialsException(BaseErrorCode.SMS_CODE_INPUT_ERROR.getErrDispose());
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

}
